<?php

namespace Push;

use Push\Exceptions\CommonException;
use Push\Exceptions\ConnectionException;
use Push\Interfaces\PayloadInterface;

class Service
{
    /**
     * notification发送的开始标识
     */
    const START_IDENTIFIER = -1;

    /**
     * apns返回的错误信息的报文长度
     */
    const ERROR_RESPONSE_LENGTH = 6;

    /**
     * socket上下文
     *
     * @var resource
     */
    private $context;

    /**
     * 长连接
     *
     * @var resource
     */
    private $connection;

    /**
     * 苹果服务推送地址
     *
     * @var string
     */
    private $serviceUrl = 'ssl://gateway.push.apple.com:2195';

    /**
     * 证书路径
     *
     * @var string
     */
    private $certificate;

    /**
     * 证书私钥密码
     *
     * @var string
     */
    private $passphrase;

    /**
     * 连接超时时间
     *
     * @var int
     */
    private $connTimeout = 60;

    /**
     * 不太清楚干啥用的
     *
     * @var int
     */
    private $expiry = 0;

    /**
     * device token列表
     *
     * @var array
     */
    private $tokens = [];

    /**
     * 载荷
     *
     * @var PayloadInterface
     */
    private $payload;

    /**
     * 每次发送完notifications和断开连接的时间间隔。单位：秒
     *
     * @var int
     */
    private $waitTime = 1;

    /**
     * identifier文件保存路径
     *
     * @var string
     */
    private $identifierFile = '/var/tmp/ios_push_v2_identifier';

    /**
     * report文件保存路径
     *
     * @var string
     */
    private $reportFile = '/var/tmp/ios_push_v2_report';

    /**
     * Service constructor.
     * @param array $config
     * @throws CommonException
     */
    public function __construct($config)
    {
        $this->configure($config);
    }

    /**
     * 初始化ssl连接配置
     *
     * @param $config
     * @throws CommonException
     */
    private function configure($config)
    {
        if (isset($config['serviceUrl'])) {
            $this->serviceUrl = $config['serviceUrl'];
        }
        if (isset($config['certificate'])) {
            $this->certificate = $config['certificate'];
        } else {
            throw new CommonException('未设置数字证书！');
        }
        if (isset($config['passPhrase'])) {
            $this->passPhrase = $config['passPhrase'];
        } else {
            throw new CommonException('未设置数字证书私钥密码！');
        }
        if (isset($config['timeout'])) {
            $this->timeout = $config['timeout'];
        }
    }

    /**
     * 初始化socket上下文，并重置identifier和report
     */
    private function init()
    {
        $this->context = stream_context_create();
        stream_context_set_option($this->context, 'ssl', 'local_cert', $this->certificate);
        stream_context_set_option($this->context, 'ssl', 'passphrase', $this->passphrase);

        @unlink($this->reportFile);
        @unlink($this->identifierFile);
        file_put_contents($this->identifierFile, self::START_IDENTIFIER);
    }

    /**
     * 开始向苹果客户端推送通知
     *
     * @throws CommonException
     * @throws ConnectionException
     */
    public function push()
    {
        $this->init();
        while (file_exists($this->identifierFile)) {
            $this->connect();
            $this->asyncNotify();
        }
    }

    /**
     * 获取当前推送失败的notification的唯一标识
     *
     * @return bool|string
     * @throws CommonException
     */
    private function currentIdentifier()
    {
        $identifier = @file_get_contents($this->identifierFile);
        if (!isset($identifier) || $identifier === false || $identifier === '') {
            throw new CommonException('获取identifier失败！');
        }
        return trim($identifier);
    }

    /**
     * 异步读、写socket流
     *
     * @throws CommonException
     */
    private function asyncNotify()
    {
        $pid = pcntl_fork();
        if ($pid == -1) {
            throw new CommonException('启动子进程失败！');
        } else if ($pid) {
            $this->writeToStream($this->currentIdentifier());
            sleep($this->waitTime);

            $this->disconnect();
            pcntl_wait($status);
        } else {
            $this->listenStream();
            $this->terminateChildProcess();
        }
    }

    /**
     * 将要推送的消息输出到socket流
     *
     * @param int $index
     */
    private function writeToStream($index = -1)
    {
        $total = count($this->tokens);
        for ($i = $index + 1; $i < $total; $i++) {
            $message = $this->pack($i, $this->tokens[$i], $this->payload->payload());
            try {
                $size = @fwrite($this->connection, $message, strlen($message));
                if ($size === false) {
                    break;
                }
            } catch (\Exception $ex) {
                break;
            }
        }
    }

    /**
     * 监听socket输入流
     */
    private function listenStream()
    {
        while (!feof($this->connection)) {
            $response = fread($this->connection, self::ERROR_RESPONSE_LENGTH);
            if ($response) {
                $errMsg = $this->unpack($response);
                $this->recordIdentifier($errMsg['identifier']);
                $this->addReport($errMsg, $this->tokens[$errMsg['identifier']]);
            } else {
                @unlink($this->identifierFile);
            }
        }
    }

    /**
     * 保存identifier到文件
     *
     * @param int $identifier
     */
    private function recordIdentifier($identifier)
    {
        file_put_contents($this->identifierFile, $identifier);
    }

    /**
     * 结束php进程
     */
    private function terminateChildProcess()
    {
        exit(0);
    }

    /**
     * 连接apns
     *
     * @throws ConnectionException
     */
    private function connect()
    {
        $this->connection = @stream_socket_client(
            $this->serviceUrl, $errCode, $errMsg, $this->connTimeout, STREAM_CLIENT_CONNECT, $this->context);

        if ($errCode || !$this->connection) {
            throw new ConnectionException('apns连接失败 #' . $errCode . ':' . $errMsg);
        }
    }

    /**
     * 断开连接
     */
    public function disconnect()
    {
        if ($this->connection && is_resource($this->connection)) {
            @fclose($this->connection);
        }
    }

    /**
     * 重置连接
     *
     * @throws ConnectionException
     */
    private function resetConnect()
    {
        $this->disconnect();
        $this->connect();
    }

    /**
     * 打包要推送的notification
     *
     * @param int $key
     * @param string $token
     * @param PayloadInterface $payload
     * @return string
     */
    private function pack($key, $token, $payload)
    {
        $command = chr(1);
        $identifier = pack('N', $key);
        $expiry = pack('N', $this->expiry);
        $tokenLength = pack('n', strlen($token) / 2);
        $token = pack('H*', $token);
        $payloadLength = pack('n', strlen($payload));

        return $command . $identifier . $expiry . $tokenLength . $token . $payloadLength . $payload;
    }

    /**
     * 解包推送notification返回的错误信息
     *
     * @param $binary
     * @return array
     */
    private function unpack($binary)
    {
        return [
            'command' => unpack('C', substr($binary, 0, 1))[1],
            'status' => unpack('C', substr($binary, 1, 1))[1],
            'identifier' => unpack('N', substr($binary, 2, 4))[1],
        ];
    }

    /**
     * 添加token到列表
     *
     * @param string $token
     */
    public function addToken($token)
    {
        if (!isset($this->tokens)) {
            $this->tokens = [];
        }
        $this->tokens[] = $token;
    }

    /**
     * 添加错误报告
     *
     * @param mixed $response
     * @param string $token
     */
    private function addReport($response = null, $token = null)
    {
        $report = [
            'token' => $token,
            'response' => $response
        ];
        file_put_contents($this->reportFile, json_encode($report, JSON_UNESCAPED_UNICODE) . PHP_EOL, FILE_APPEND);
    }

    /**
     * 获取错误报告
     *
     * @return array
     */
    public function getReports()
    {
        $reports = [];
        $fp = @fopen($this->reportFile, 'r');
        if ($fp) {
            while (!feof($fp)) {
                $report = json_decode(fgets($fp), true);
                if ($report) {
                    $reports[] = $report;
                }
            }
        }
        return $reports;
    }

    /**
     * @return string
     */
    public function getServiceUrl()
    {
        return $this->serviceUrl;
    }

    /**
     * @param string $serviceUrl
     */
    public function setServiceUrl($serviceUrl)
    {
        $this->serviceUrl = $serviceUrl;
    }

    /**
     * @return string
     */
    public function getCertificate()
    {
        return $this->certificate;
    }

    /**
     * @param string $certificate
     */
    public function setCertificate($certificate)
    {
        $this->certificate = $certificate;
    }

    /**
     * @return string
     */
    public function getPassphrase()
    {
        return $this->passphrase;
    }

    /**
     * @param string $passphrase
     */
    public function setPassphrase($passphrase)
    {
        $this->passphrase = $passphrase;
    }

    /**
     * @return int
     */
    public function getConnTimeout()
    {
        return $this->connTimeout;
    }

    /**
     * @param int $connTimeout
     */
    public function setConnTimeout($connTimeout)
    {
        $this->connTimeout = $connTimeout;
    }

    /**
     * @return int
     */
    public function getExpiry()
    {
        return $this->expiry;
    }

    /**
     * @param int $expiry
     */
    public function setExpiry($expiry)
    {
        $this->expiry = $expiry;
    }

    /**
     * @return array
     */
    public function getTokens()
    {
        return $this->tokens;
    }

    /**
     * @param array $tokens
     */
    public function setTokens($tokens)
    {
        $this->tokens = $tokens;
    }

    /**
     * @return PayloadInterface
     */
    public function getPayload()
    {
        return $this->payload;
    }

    /**
     * @param PayloadInterface $payload
     */
    public function setPayload($payload)
    {
        $this->payload = $payload;
    }

    /**
     * @return int
     */
    public function getWaitTime()
    {
        return $this->waitTime;
    }

    /**
     * @param int $waitTime
     */
    public function setWaitTime($waitTime)
    {
        $this->waitTime = $waitTime;
    }

    /**
     * @return string
     */
    public function getIdentifierFile()
    {
        return $this->identifierFile;
    }

    /**
     * @param string $identifierFile
     */
    public function setIdentifierFile($identifierFile)
    {
        $this->identifierFile = $identifierFile;
    }

    /**
     * @return string
     */
    public function getReportFile()
    {
        return $this->reportFile;
    }

    /**
     * @param string $reportFile
     */
    public function setReportFile($reportFile)
    {
        $this->reportFile = $reportFile;
    }
}
